﻿/* Copyright (c) 2011-2012, Zetatron Consulting
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

* Neither the name of Zetatron Consulting nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using AionCxxLibrary.Packets;
using AionCxxLibrary.Utilities;
using System.IO;
using AionCxxLibrary.Models;

namespace AionCxxLibrary.Network
{
    public abstract class AionConnection
    {
        protected TcpClient client;
        public AionCrypter Crypter;
        protected byte[] ReceiveBuffer;

        public Account account;

        Queue<AionServerPacket> PacketSendQueue = new Queue<AionServerPacket>();
        bool WriteInterestEnabled = false;
        protected bool Ready = false;

        MemoryStream ReceiveStream;

        public AionConnection(TcpClient c, AionCrypter crypter)
        {
            client = c;
            ReceiveBuffer = new byte[8192];
            ReceiveStream = new MemoryStream(8192 * 4);
            Crypter = crypter;   
        }

        public void SetClient(TcpClient client)
        {
            this.client = client;
        }

        public string GetRemoteIP()
        {
            return ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString();
        }

        public void Close()
        {
            Log.Debug("ServerClose " + GetRemoteIP());
            OnDisconnect();
            client.Close();
        }

        public virtual void OnConnect()
        {
            BeginRead();
        }

        void BeginRead()
        {
            if (client != null && client.Client != null && client.Connected)
            {
                try
                {
                    client.Client.BeginReceive(ReceiveBuffer, 0, 8192, SocketFlags.None, new AsyncCallback(EndRead), ReceiveBuffer);
                }
                catch (Exception)
                {
                    Close();
                }
            }
        }

        public virtual void OnDisconnect()
        {

        }

        bool ReadInterestEnabled = false;

        void EnableReadInterest()
        {
            if (ReadInterestEnabled)
                return;
            lock (ReceiveStream)
            {
                ReadInterestEnabled = true;
                
                // Read First Packet Size then go back at initial position.
                ushort size = BitConverter.ToUInt16(new byte[] { (byte)ReceiveStream.ReadByte(), (byte)ReceiveStream.ReadByte() }, 0);
                ReceiveStream.Position -= 2;

                while (size > 0 && size <= (ReceiveStream.Length - ReceiveStream.Position))
                {
                    //Log.Debug("Read: " + size + "-byte packet.");

                    // Read Packet Data including length
                    byte[] data = new byte[size];
                    ReceiveStream.Read(data, 0, size);

                    if (Crypter != null)
                    {
                        // Decrypt the chunk
                        Crypter.Decrypt(ref data, 2, size - 2);
                    }

                    // Process packet.
                    MemoryStream stream = new MemoryStream(data, 2, data.Length - 2, false);

                    byte opcode = (byte)stream.ReadByte();
                    Type packetType = Packets.Packets.instance.GetClientPacketType(opcode);

                    if (Crypter is WorldCrypter)
                        stream.Position += 4;

                    if (packetType == null)
                    {
                        Log.Warn("Unknown packet received from AION client, opcode: " + string.Format("{0:X}", opcode) + " - size: " + (size - 2));
                    }
                    else
                    {
                        AionClientPacket pkt = (AionClientPacket)Activator.CreateInstance(packetType);
                        pkt.Write(stream.ToArray(), (int)stream.Position, (int)(stream.Length - stream.Position));
                        pkt.Position = 0;
                        stream.Close();

                        Log.Debug("PktRecv " + GetRemoteIP() + ": " + pkt.GetType().Name + "(" + pkt.Length + ")");
                        pkt.Connection = this;
                        pkt.Opcode = opcode;

                        try
                        {
                            pkt.ProcessPacket();
                        }
                        catch (Exception e)
                        {
                            Log.Error("Cannot process client packet!", e);
                        }

                        pkt.Close();
                    }

                    if (ReceiveStream.Length - ReceiveStream.Position > 2)
                    {
                        size = BitConverter.ToUInt16(new byte[] { (byte)ReceiveStream.ReadByte(), (byte)ReceiveStream.ReadByte() }, 0);
                        ReceiveStream.Position -= 2;
                    }
                    else
                        size = 0;
                }

                int remaining = (int)(ReceiveStream.Length - ReceiveStream.Position);
                if (remaining > 0)
                {
                    Log.Debug("Remaining Bytes : " + remaining);
                    // Read Latest Bytes
                    byte[] remdata = new byte[remaining];
                    ReceiveStream.Read(remdata, 0, remaining);
                    ReceiveStream.Position = 0;
                    ReceiveStream.Write(remdata, 0, remdata.Length);
                }
                else
                    ReceiveStream.Position = 0;

                ReadInterestEnabled = false;
            }
        }

        void EndRead(IAsyncResult result)
        {
            lock (ReceiveBuffer)
            {
                try
                {
                    int received = 0;

                    try
                    {
                        received = client.Client.EndReceive(result);
                    }
                    catch (Exception)
                    {
                        OnDisconnect();
                        return;
                    }

                    if (received <= 0)
                    {
                        OnDisconnect();
                        return;
                    }

                    if (!Ready)
                    {
                        Log.Warn("Received a " + received + "-byte packet while connection not ready to accept packets.");
                        return;
                    }

                    // This is the full set of data.
                    // May contain several packets.
                    byte[] data = (byte[])result.AsyncState;

                    lock (ReceiveStream)
                    {
                        ReceiveStream.Write(data, 0, received);
                    }

                    if (!ReadInterestEnabled)
                    {
                        ReceiveStream.SetLength(ReceiveStream.Position);
                        ReceiveStream.Position = 0;
                        EnableReadInterest();
                    }
                }
                catch (Exception e)
                {
                    Log.Error("Error while processing client data!", e);
                }

                BeginRead();
            }
        }
        
        public void SendPacket(AionServerPacket packet)
        {
            lock (PacketSendQueue)
            {
                PacketSendQueue.Enqueue(packet);
                if (!WriteInterestEnabled)
                    EnableWriteInterest();
            }
        }

        public void SendPacket(AionServerPacket packet, bool closeAfterPacket)
        {
            SendPacket(packet);
            if (closeAfterPacket)
                SendPacket(null);
        }

        void EnableWriteInterest()
        {
            WriteInterestEnabled = true;
            while (PacketSendQueue.Count > 0)
            {
                AionServerPacket pkt = PacketSendQueue.Dequeue();
                if (pkt == null)
                {
                    Close();
                    return;
                }
                BeginWrite(pkt);
            }
            WriteInterestEnabled = false;
        }

        protected void BeginWrite(AionServerPacket packet)
        {
            if (packet == null)
                return;
            try
            {
                // Here, packet is ready to be sent
                packet.Connection = this;
                packet.Position = 0;

                if (!Packets.Packets.instance.HasServerOpcode(packet.GetType()))
                {
                    Log.Warn("No opcode defined for " + packet.GetType().Name);
                    return;
                }

                byte opcode = Packets.Packets.instance.GetServerPacketOpcode(packet.GetType());

                packet.Opcode = opcode;

                packet.WriteShort(0); // Future length
                
                if (Crypter != null && Crypter is WorldCrypter)
                {
                    byte cryptedOpcode = (byte)((opcode + 0xAE) ^ 0xEE);
                    packet.WriteByte(cryptedOpcode); // Crypted World Opcode
                    byte sw = 0;
                    if (Packets.Packets.HighPacketsOpcs.Contains(opcode))
                        sw = 1;
                    packet.WriteByte(sw);
                    packet.WriteByte(0x54);
                    packet.WriteByte((byte)(~cryptedOpcode));
                    packet.WriteByte((byte)(0xFF - sw));
                }
                else
                    packet.WriteByte(opcode); // Opcode

                packet.WritePacket(); // Write contents of packet

                int length = (int)packet.Position;

                // Encrypt packet
                byte[] result;                
                if(Crypter is WorldCrypter)
                    result = new byte[length];
                else
                    result = new byte[length * 4];

                Array.Copy(packet.ToArray(), 0, result, 0, length);

                if (Crypter != null && packet.Crypted)
                {
                    //lock (Crypter)
                    //{
                        length = Crypter.Encrypt(ref result, 2, length - 2) + 2;
                    //}
                }

                packet.Position = 0;
                packet.WriteShort((short)length);
                packet.Write(result, 2, length-2);
                
                // Send in socket
                client.Client.BeginSend(packet.ToArray(), 0, length, SocketFlags.None, new AsyncCallback(EndWrite), packet);

                if (!Ready)
                    Ready = true;

            }
            catch (Exception e)
            {
                Log.Error("Error while writing to data sink!", e);
            }
        }

        void EndWrite(IAsyncResult result)
        {
            if (client != null && client.Client != null && client.Client.Connected)
            {
                int sent = client.Client.EndSend(result);
                AionServerPacket pkt = (AionServerPacket)result.AsyncState;
                Log.Debug("PktSend " + GetRemoteIP() + ": " + pkt.GetType().Name + " (" + pkt.Length + ")");
            }
        }

    }
}
